Adding a new API
Use this checklist when deploying a new Python API to the server. Complete steps in order. Replace placeholders:
| Placeholder | Example |
|---|---|
<api-name> | customer-api |
<port> | 8001 (see port table in README) |
<git-url> | git@github.com:org/customer-api.git |
<subdomain> | customer.example.com |
<module>:<app> | app.main:app (FastAPI/Uvicorn entrypoint) |
Native vs Docker
This checklist is the native path (venv + systemd). If the API runs in Docker, skip Steps 2–3 (venv) and use 05-docker-strategy.md plus the repo’s Dockerfile / compose file. Steps 4 (env file), 6 (Nginx), and 7 (documentation) still apply.
Prerequisites
- Server base packages installed (python3, python3-venv, git, nginx)
-
apisvcsystem user exists (see 01-directory-layout.md) - Port number chosen and recorded in README.md
- Git deploy key configured (see 07-code-management-and-git.md)
- Subdomain DNS planned (can point to LAN IP during setup)
Step 1 — Create directories
API=<api-name>
sudo mkdir -p /opt/apis/$API
sudo mkdir -p /opt/venvs/$API
sudo mkdir -p /etc/$API
sudo mkdir -p /var/log/$API
sudo chown apisvc:apisvc /opt/apis/$API
sudo chown apisvc:apisvc /opt/venvs/$API
sudo chown root:apisvc /etc/$API
sudo chmod 750 /etc/$API
sudo chown apisvc:apisvc /var/log/$API
Step 2 — Clone application code
Configure a read-only deploy key first — full copy-paste commands in 07-code-management-and-git.md.
Then clone (as your SSH login user; not apisvc):
API=<api-name>
GITHUB_ORG=<org>
GITHUB_REPO=<repo-name>
git clone git@github.com-${API}:${GITHUB_ORG}/${GITHUB_REPO}.git \
/opt/apis/${API}
sudo chown -R apisvc:apisvc /opt/apis/${API}
sudo chmod -R g+rwX /opt/apis/${API}
Example for the first API:
API=dwd-api-fastapi
GITHUB_ORG=gqc
GITHUB_REPO=dwd-api-fastapi
git clone git@github.com-${API}:${GITHUB_ORG}/${GITHUB_REPO}.git \
/opt/apis/${API}
sudo chown -R apisvc:apisvc /opt/apis/${API}
sudo chmod -R g+rwX /opt/apis/${API}
If the repository already exists and you are re-running setup, use git pull instead.
Verify the entrypoint module exists (FastAPI example):
ls /opt/apis/<api-name>/app/main.py # paths vary by repo
Note the correct Uvicorn target: <module>:<app> (e.g. app.main:app).
Step 3 — Create virtual environment and install dependencies
API=<api-name>
sudo -u apisvc python3 -m venv /opt/venvs/$API
sudo -u apisvc /opt/venvs/$API/bin/pip install --upgrade pip
sudo -u apisvc /opt/venvs/$API/bin/pip install -r /opt/apis/$API/requirements.txt
If the app uses Uvicorn but it is not in requirements.txt:
sudo -u apisvc /opt/venvs/$API/bin/pip install uvicorn
For Azure Storage:
sudo -u apisvc /opt/venvs/$API/bin/pip install azure-storage-blob
Step 4 — Create environment file
Copy the template and fill in values:
API=<api-name>
sudo cp templates/api.env.example /etc/$API/$API.env
sudo chmod 640 /etc/$API/$API.env
sudo chown root:apisvc /etc/$API/$API.env
sudo nano /etc/$API/$API.env
Required variables depend on the application. Common entries:
ENV=production- Azure credentials (connection string or service principal)
- Any API keys the app reads from the environment
Test that the app starts with the env file (manual smoke test):
API=<api-name>
PORT=<port>
set -a && source /etc/$API/$API.env && set +a
sudo -u apisvc /opt/venvs/$API/bin/uvicorn <module>:<app> \
--host 127.0.0.1 --port $PORT
Press Ctrl+C after confirming it binds without errors. Then proceed to systemd.
Step 5 — Install systemd service
API=<api-name>
PORT=<port>
sudo cp templates/api.service.template /etc/systemd/system/$API.service
sudo sed -i "s/<api-name>/$API/g" /etc/systemd/system/$API.service
sudo sed -i "s/<port>/$PORT/g" /etc/systemd/system/$API.service
sudo sed -i "s/<module>:<app>/<module>:<app>/g" /etc/systemd/system/$API.service
sudo systemctl daemon-reload
sudo systemctl enable $API
sudo systemctl start $API
sudo systemctl status $API
Verify locally:
curl -s http://127.0.0.1:<port>/health
# or /docs for FastAPI, or / depending on the app
Step 6 — Configure Nginx reverse proxy
When DNS and SSL are ready (or for LAN testing with HTTP only):
API=<api-name>
SUB=<subdomain>
sudo cp templates/nginx-site.template /etc/nginx/sites-available/$API
sudo sed -i "s/<api-name>/$API/g" /etc/nginx/sites-available/$API
sudo sed -i "s/<subdomain>/$SUB/g" /etc/nginx/sites-available/$API
sudo sed -i "s/<port>/<port>/g" /etc/nginx/sites-available/$API
sudo ln -sf /etc/nginx/sites-available/$API /etc/nginx/sites-enabled/$API
sudo nginx -t
sudo systemctl reload nginx
For SSL, add certificate paths to the Nginx block (see 04-nginx-reverse-proxy.md).
Step 7 — Update documentation
On this workstation, edit README.md:
- Add row to port allocation table
- Fill in "First API" or add an "Deployed APIs" table
- Note any repo-specific quirks (custom health path, extra env vars)
Step 8 — Post-deploy verification
# Service running
systemctl is-active <api-name>
# Listening on localhost only
ss -tlnp | grep <port>
# Through Nginx (when configured)
curl -I https://<subdomain>/health
Confirm UFW is not exposing <port> (when UFW is enabled):
sudo ufw status
# Should show 22, 80, 443 only — not 8001, 8002, etc.
Updating an existing API (routine deploy)
Manual deploy pattern until CI/CD is chosen:
API=<api-name>
cd /opt/apis/${API}
git pull
sudo -u apisvc /opt/venvs/${API}/bin/pip install -r requirements.txt
sudo systemctl restart ${API}
journalctl -u ${API} -n 50 --no-pager
If requirements.txt did not change, the pip install step is optional but harmless.
Troubleshooting
| Symptom | Check |
|---|---|
| Service fails immediately | journalctl -u <api-name> -n 100 --no-pager |
| Permission denied on git pull | Ownership under /opt/apis/<api-name>; deploy user group |
| Import errors | Venv path in systemd unit; pip install -r requirements.txt |
| 502 from Nginx | API running? curl http://127.0.0.1:<port>/; check Nginx error log |
| Env vars not loaded | EnvironmentFile= path in unit; file mode 640 and group apisvc |
Checklist summary (printable)
[ ] Directories created with correct ownership
[ ] Git repository cloned to /opt/apis/<api-name>/
[ ] Venv at /opt/venvs/<api-name>/ with dependencies installed
[ ] /etc/<api-name>/<api-name>.env created (mode 640)
[ ] Manual uvicorn smoke test passed
[ ] systemd unit installed, enabled, and active
[ ] curl to 127.0.0.1:<port> succeeds
[ ] Nginx site configured and reloaded
[ ] README.md updated on workstation
[ ] Port not exposed in UFW